package ytmc.converter.ffmpeg;

import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import ytmc.TimePeriod;
import ytmc.converter.*;
import ytmc.setup.*;


/**
 *
 * @author Kicok
 */
public class FFmpegConverter extends Converter
{
    private static final String FFMPEG_EXE_PATH = "ffmpeg\\ffmpeg.exe";

    private Timer updater;

    private int taskID;
    private ConversionTask task;
    private Date conversionStart;

    private double duration;
    private double elapsed;
    private InputHandler inputHandler;


    public FFmpegConverter( ConversionTask task, int taskID )
    {
        this.task = task;
        this.taskID = taskID;

        this.updater = new Timer( true );

        removeOutputFileIfExists();
    }

    private void removeOutputFileIfExists()
    {
        task.getOutputFile().delete();
    }


    @Override
    public ConversionTask getTask()
    {
        return task;
    }


    @Override
    public void convertAsync()
    {
        Thread worker = new Thread( new Runnable() {

            @Override
            public void run()
            {
                try
                {
                    start();
                    updater.schedule( new UpdateTask(), 500, 500 );

                    List<String> command = buildCommandList();
                    ProcessBuilder pb = new ProcessBuilder( command );
                    Process ffmpeg = pb.start();

                    DoingNothingInputHandler dnih = new DoingNothingInputHandler( ffmpeg.getInputStream() );
                    dnih.setDaemon( true );
                    dnih.start();

                    inputHandler = new InputHandler( ffmpeg.getErrorStream() );
                    inputHandler.setDaemon( true );
                    inputHandler.start();
                    inputHandler.join();

                    successfulFinish();
                }
                catch( Exception e )
                {
                    System.out.println( "Wystąpił błąd podczas konwersji" );
                    System.out.println( e );
                    failedFinish();
                }
                finally
                {
                    updater.cancel();
                    setChanged();
                    notifyObservers( taskID );
                }
            }
        } );

        worker.setDaemon( true );
        worker.start();
    }

    private void start()
    {
        task.setProgress( 0 );
        task.setStatus( ConversionStatus.RUNNING );

        conversionStart = new Date();

        setChanged();
        notifyObservers( taskID );
    }

    private void successfulFinish()
    {
        task.setProgress( 100 );
        task.setRemainingTime( 0 );
        task.setStatus( ConversionStatus.FINISHED );

        setChanged();
        notifyObservers( taskID );
    }

    private void failedFinish()
    {
        task.setProgress( 0 );
        task.setRemainingTime( -1 );
        task.setStatus( ConversionStatus.FAILED );

        setChanged();
        notifyObservers( taskID );
    }


    private List<String> buildCommandList()
    {
        List<String> commands = new ArrayList<String>();
        commands.add( FFMPEG_EXE_PATH );
        commands.add( "-i" );
        commands.add( getTask().getInputFile().getAbsolutePath() );

        if( getTask().getType() == ConversionType.VIDEO )
        {
            if( getTask().getVideoResolution() != VideoResolution.RES_ORIGINAL )
            {
                commands.add( "-s" );
                commands.add( getTask().getVideoResolution().getFriendlyName() );
            }
            if( getTask().getVideoFrameRate() != 0 )
            {
                commands.add( "-r" );
                commands.add( Integer.toString( getTask().getVideoFrameRate() ) );
            }

            commands.add( "-sameq" );
            commands.add( "-ab" );
            commands.add( "192k" );
        }
        else
        {
            if( getTask().getAudioQuality() != AudioQuality.QUAL_ORIGINAL )
            {
                commands.add( "-ab" );
                commands.add( getTask().getAudioQuality().getFriendlyName().replaceFirst( " kbps$", "k" ) );
            }
            else
            {
                commands.add( "-ab" );
                commands.add( "192k" );
            }

            commands.add( "-vn" );
            commands.add( "-ac" );
            commands.add( "2" );
        }

        commands.add( getTask().getOutputFile().getAbsolutePath() );
        return commands;
    }


    private class UpdateTask extends TimerTask
    {
        @Override
        public void run()
        {
            determineProgress();

            setChanged();
            notifyObservers( taskID );
        }

        private void determineProgress()
        {
            if( inputHandler != null )
            {
                if( duration == 0 )
                {
                    if( inputHandler.isDurationFound() )
                    {
                        duration = new TimePeriod( inputHandler.getDuration() ).toSeconds();
                    }
                }
                else
                {
                    elapsed = Double.parseDouble( inputHandler.getElapsed() );
                    getTask().setProgress( (int)( 100 * elapsed / duration ) );

                    long currentConversionTime = new Date().getTime() - conversionStart.getTime();
                    long expectedConversionTime = (long)( currentConversionTime * 100.0 / getTask().getProgress() );
                    getTask().setRemainingTime( (int)( ( expectedConversionTime - currentConversionTime ) / 1000 ) );
                }
            }
        }
    }


    /**
     * Reads duration and conversion elapsed time from FFmpeg standard error stream
     * Must work in other thread or FFmpeg process will hang
     */
    private class InputHandler extends Thread
    {
        private InputStream input;

        private boolean isFinished;
        private String duration;
        private String elapsed;
        private boolean durationFound;

        private Pattern durationPattern;
        private Pattern elapsedPattern;


        InputHandler( InputStream input )
        {
            super( "FFmpeg input handler (err)" );

            this.input = input;

            durationPattern = Pattern.compile( "^\\s+Duration:\\s([0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,3})?)", Pattern.CASE_INSENSITIVE );
            elapsedPattern = Pattern.compile( "^frame=\\s*[0-9]+ fps=.*?time=([0-9]+\\.[0-9]+) ", Pattern.CASE_INSENSITIVE );
        }


        @Override
        public void run()
        {
            try
            {
                String line;
                BufferedReader br = null;

                try
                {
                    br = new BufferedReader( new InputStreamReader( input ) );
                    while( ( line = br.readLine() ) != null )
                    {
                        if( !durationFound )
                        {
                            Matcher m = durationPattern.matcher( line );
                            if( m.find() )
                            {
                                duration = m.group( 1 );
                                durationFound = true;
                            }
                        }
                        else
                        {
                            Matcher m = elapsedPattern.matcher( line );
                            if( m.find() )
                            {
                                elapsed = m.group( 1 );
                            }
                        }
                    }
                }
                finally
                {
                    isFinished = true;

                    if( br != null )
                    {
                        br.close();
                    }
                }
            }
            catch( Exception e )
            {
                System.out.println( "Wystąpił błąd podczas odczytywania wyjścia (err) programu ffmpeg" );
                System.out.println( e );
            }
        }


        public boolean isFinished()
        {
            return isFinished;
        }

        public String getDuration()
        {
            return duration;
        }

        public boolean isDurationFound()
        {
            return durationFound;
        }

        public String getElapsed()
        {
            return elapsed;
        }
    }


    /**
     * Reads data from FFmpeg standard output stream (just in case)
     * Must work in other thread or FFmpeg process will hang
     */
    private class DoingNothingInputHandler extends Thread
    {
        private InputStream input;

        public DoingNothingInputHandler( InputStream input )
        {
            super( "FFmpeg input handler (out)" );

            this.input = input;
        }


        @Override
        public void run()
        {
            try
            {
                while( input.read() != -1 );
            }
            catch( Exception e )
            {
                System.out.println( "Wystąpił błąd podczas odczytywania wyjścia (out) programu ffmpeg" );
                System.out.println( e );
            }
        }
    }
}